Skip to content

Fall back to English for messages with damaged format placeholders - #14666

Closed
shashb27 wants to merge 5 commits into
sphinx-doc:masterfrom
shashb27:fix/14664-safe-format-translations
Closed

shashb27 wants to merge 5 commits into
sphinx-doc:masterfrom
shashb27:fix/14664-safe-format-translations

Conversation

@shashb27

@shashb27 shashb27 commented Sep 2, 2026 •

Copy link
Copy Markdown

Refs #14664.

Problem

As detailed in #14664, some translated catalogues (e.g. el, fa) contain msgstrs with damaged str.format placeholders:

>>> import gettext
>>> t = gettext.translation('sphinx', 'sphinx/locale', languages=['el'])
>>> t.gettext("The config value `{name}' has type `{current.__name__}', "
...           "defaults to `{default.__name__}'.").format(name='x', current=int, default=str)
KeyError: 'current__name__'

Six call sites format translated text eagerly, so a single damaged placeholder in any locale kills the build with a KeyError that surfaces as an ExtensionError blaming an extension (sphinx/config.py x3, sphinx/_cli/__init__.py x2, sphinx/transforms/i18n.py).

Approach

Add sphinx.locale.safe_format: pass the untranslated message and the format arguments; it formats the translation and falls back to the original English message when the translation is damaged.

Damage detection compares the set of str.format field names between the msgid and its translation, plus a try/except around the format call:

  • Renamed or added placeholders (e.g. {current.__name__} -> {current__name__}) and dropped placeholders are both caught. A plain try/except alone cannot catch dropped placeholders, because str.format silently ignores surplus arguments and would emit an incomplete message.
  • Comparing sets (not sequences) keeps legitimate translator reordering ({1} ... {0}) working.
  • When no translation is active, behaviour is identical to plain .format (the untranslated message formats itself).

Affected messages degrade to an English warning instead of a dead build. The catalogue data itself (the Transifex half of #14664) is not touched here ΓÇö that needs someone with Transifex access, per the discussion in the issue.

Tests

  • tests/test_locale.py: unit tests for the helper (healthy translation, renamed keyword, dropped positional, mixed corruption, reordered placeholders, other namespaces).
  • tests/test_config/test_config.py: regression test driving a corrupted catalogue through check_confval_types, asserting the English fallback message.
  • The full tests/test_intl/test_intl.py suite passes (61 tests) ΓÇö the compare_references warnings now format through the helper.

AI Disclosure

Per the PR template: the opencode coding agent (GLM) was used in preparing this pull request — to survey the translation catalogues for damaged str.format placeholders, draft the safe_format fallback, and write the tests. I reviewed the change and validated it locally (full test_intl suite green).

Some translated catalogues contain msgstrs with damaged str.format placeholders. Six call sites formatted translated text eagerly, so a single damaged placeholder in any locale crashed the build with a KeyError.

Add sphinx.locale.safe_format, which formats the translation and falls back to the untranslated message when the placeholder sets do not match or formatting raises, and route the eager call sites through it.

Refs sphinx-doc#14664
@shashb27

shashb27 commented Sep 2, 2026

Copy link
Copy Markdown
Author

Heads-up on the red test matrix: the failing set here (test_viewcode*, test_default_highlight, test_html_code_role, the two test_additional_targets_* intl tests, plus the wrongenc.inc checkout encode errors) matches what every currently-open PR hits — the same jobs on #14654 and #14662 fail with the identical test list, and the last green main.yml run on master is from Aug 11. So this looks like dependency drift in the CI environment rather than something from this branch.

For what it's worth, the suites covering the code this branch touches pass in CI (test_config shows green in the matrix) and locally: tests/test_locale.py, tests/test_config/test_config.py, tests/test_command_line.py, and the full tests/test_intl/test_intl.py (61/61).

@shashb27

Copy link
Copy Markdown
Author

Quick status update now that #14611 has landed: the branch is updated and the test matrix is green, apart from the queue-wide Docutils HEAD and LaTeX jobs that are still red across the open PR queue. Ready for review whenever someone has a window.

@shashb27

Copy link
Copy Markdown
Author

Ping for review when someone has a cycle — this fixes the safe_format placeholder-set fallback discussed in the issue (code half; Transifex half remains separate). Branch is conflict-free against current master.

@m-aciek m-aciek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With those changes python babel_runner.py extract may miss those strings on which we use safe_format. Maybe we can configure babel for a new function name, or fix it in some other way?

Otherwise those changes look good to me, thank you.

Teach the extraction keywords about `safe_format` (its first argument is the msgid) and `_NodeUpdater.compare_references` (its third argument is the untranslated warning message), so that the strings moved behind `safe_format` in sphinx-doc#14664 keep appearing in `sphinx/locale/sphinx.pot`.
@shashb27

Copy link
Copy Markdown
Author

Thank you for catching this — confirmed, and it's actually worse than "may miss": I ran python utils/babel_runner.py extract on the branch without any fix, and all nine msgids that the PR moves behind safe_format (two in sphinx/_cli/__init__.py, three in sphinx/config.py, and the four compare_references warnings in sphinx/transforms/i18n.py) drop out of sphinx/locale/sphinx.pot entirely — so the next Transifex sync would push them as obsolete.

I went with your "configure babel" suggestion, in 440f111: KEYWORDS in utils/babel_runner.py now knows the new names:

KEYWORDS = {
    **DEFAULT_KEYWORDS,
    '_': None,
    '__': None,
    'safe_format': None,
    'compare_references': (3,),
}

safe_format's first argument is the untranslated msgid, so the default spec covers it; compare_references receives its warning message as the third argument, hence (3,) (babel wants a tuple for explicit argument positions).

Verified by re-running extraction on the branch: the regenerated template contains all nine msgids again, with nothing else lost (861 → 870 msgids, 9 new / 0 lost against the unfixed run). tests/test_locale.py and tests/test_config/test_config.py pass locally (67 passed), and ruff is clean.

@shashb27

Copy link
Copy Markdown
Author

@m-aciek @picnixz gentle ping on this — the branch is refreshed against current master and the matrix is green, with Docutils HEAD the only red job (it is failing in its own install step on this run). The babel extract concern from the earlier review is addressed and verified in the thread above. Is there anything else you would like changed here, or could this move?

@m-aciek

m-aciek commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

After thinking it through, I think we should change strategy and prevent including incorrect translation catalogs in pull requests with updates from Transifex instead of allowing it and working it around in runtime. I'm sorry for late feedback.

@shashb27

shashb27 commented Oct 25, 2026 •

Copy link
Copy Markdown
Author

No worries about the timing — prevention at the sync is the cleaner call, and the runtime tolerance was always the workaround half. I will close this PR then; most of it (the safe_format fallback and the call-site changes) only exists to power that workaround. What I can contribute without Transifex access is the gate itself: a check that each catalogue's format placeholders match its msgid — it would have caught all three el/fa cases from the issue — wired into CI for any PR that touches the catalogues, including the sync ones. Happy to send that as a fresh PR if it is welcome.

@shashb27 shashb27 closed this Oct 25, 2026
@m-aciek

m-aciek commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

It looks like the underlying issues were fixed on Transifex about 5 months ago by Jarred Dillard.

I think a PR for CI improvement would still be welcome (though please note I'm not a maintainer). I think a good solution would be to check the catalogs when pulling with msgfmt --check-format and exclude them when failing in CI.

@shashb27

Copy link
Copy Markdown
Author

PR up as #14784. It turned out babel Catalog.check() reports zero errors on the damaged el/fa entries — it only validates messages flagged python-brace-format, which the Transifex catalogues almost never carry — so the check compares placeholders directly via string.Formatter rather than msgfmt, since the runners do not have gettext 0.23 yet. On current master it flags exactly the four messages from the issue and nothing else across all 71 catalogues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants